/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
 */
package org.olap4j.impl;

import org.olap4j.mdx.*;

import java.util.ArrayList;
import java.util.List;

/**
 * Utilities for parsing fully-qualified member names, tuples, member lists, and tuple lists.
 * 
 * <p>
 * <b>NOTE</b>: Like other classes in the org.olap4j.impl package, this class is not part of the public olap4j API. It
 * is subject to change or removal without notice. It is provided in the hope that it will be useful to implementors of
 * olap4j drivers.
 * 
 * @author jhyde
 */
public class IdentifierParser {
	// lexical states
	private static final int START = 0;
	private static final int BEFORE_SEG = 1;
	private static final int IN_BRACKET_SEG = 2;
	private static final int AFTER_SEG = 3;
	private static final int IN_SEG = 4;

	private static char charAt(String s, int pos) {
		return pos < s.length() ? s.charAt(pos) : 0;
	}

	/**
	 * Parses a list of tuples (or a list of members).
	 * 
	 * @param builder
	 *            Builder, called back when each segment, member and tuple is complete.
	 * @param string
	 *            String to parse
	 */
	public static void parseTupleList(Builder builder, String string) {
		int i = 0;
		char c;
		while ((c = charAt(string, i++)) == ' ') {
		}
		if (c != '{') {
			throw fail(string, i, "{");
		}
		while (true) {
			i = parseTuple(builder, string, i);
			while ((c = charAt(string, i++)) == ' ') {
			}
			if (c == ',') {
				// fine
			} else if (c == '}') {
				// we're done
				return;
			} else {
				throw fail(string, i, ", or }");
			}
		}
	}

	/**
	 * Parses a tuple, of the form '(member, member, ...)', and calls builder methods when finding a segment, member or
	 * tuple.
	 * 
	 * @param builder
	 *            Builder
	 * @param string
	 *            String to parse
	 * @param i
	 *            Position to start parsing in string
	 * @return Position where parsing ended in string
	 */
	public static int parseTuple(Builder builder, String string, int i) {
		char c;
		while ((c = charAt(string, i++)) == ' ') {
		}
		if (c != '(') {
			throw fail(string, i, "(");
		}
		while (true) {
			i = parseMember(builder, string, i);
			while ((c = charAt(string, i++)) == ' ') {
			}
			if (c == ',') {
				// fine
			} else if (c == ')') {
				builder.tupleComplete();
				break;
			}
		}
		return i;
	}

	public static void parseMemberList(Builder builder, String string) {
		int i = 0;
		char c = charAt(string, i);
		while (c > 0 && c <= ' ') {
			c = charAt(string, ++i);
		}
		boolean leadingBrace = false;
		boolean trailingBrace = false;
		if (c == '{') {
			leadingBrace = true;
			++i;
		}
		w: while (true) {
			i = parseMember(builder, string, i);
			c = charAt(string, i);
			while (c > 0 && c <= ' ') {
				c = charAt(string, ++i);
			}
			switch (c) {
			case 0:
				break w;
			case ',':
				// fine
				++i;
				break;
			case '}':
				// we're done
				trailingBrace = true;
				break w;
			default:
				throw fail(string, i, ", or }");
			}
		}
		if (leadingBrace != trailingBrace) {
			throw new IllegalArgumentException("mismatched '{' and '}' in '" + string + "'");
		}
	}

	public static int parseMember(Builder builder, String string, int i) {
		int k = string.length();
		int state = START;
		int start = 0;
		Builder.Syntax syntax = Builder.Syntax.NAME;
		char c;

		loop: while (i < k + 1) {
			c = charAt(string, i);
			switch (state) {
			case START:
			case BEFORE_SEG:
				switch (c) {
				case '[':
					++i;
					start = i;
					state = IN_BRACKET_SEG;
					break;

				case ' ':
					// Skip whitespace, don't change state.
					++i;
					break;

				case ',':
				case '}':
				case 0:
					break loop;

				case '.':
					// TODO: test this, case: ".abc"
					throw new IllegalArgumentException("unexpected: '.'");

				case '&':
					// Note that we have seen ampersand, but don't change state.
					++i;
					if (syntax != Builder.Syntax.NAME) {
						throw new IllegalArgumentException("unexpected: '&'");
					}
					syntax = Builder.Syntax.FIRST_KEY;
					break;

				default:
					// Carry on reading.
					state = IN_SEG;
					start = i;
					break;
				}
				break;

			case IN_SEG:
				switch (c) {
				case ',':
				case ')':
				case '}':
				case 0:
					builder.segmentComplete(null, string.substring(start, i).trim(), Quoting.UNQUOTED, syntax);
					state = AFTER_SEG;
					break loop;
				case '.':
					builder.segmentComplete(null, string.substring(start, i).trim(), Quoting.UNQUOTED, syntax);
					syntax = Builder.Syntax.NAME;
					state = BEFORE_SEG;
					++i;
					break;
				case '&':
					builder.segmentComplete(null, string.substring(start, i).trim(), Quoting.UNQUOTED, syntax);
					syntax = Builder.Syntax.NEXT_KEY;
					state = BEFORE_SEG;
					++i;
					break;
				default:
					++i;
				}
				break;

			case IN_BRACKET_SEG:
				switch (c) {
				case 0:
					throw new IllegalArgumentException("Expected ']', in member identifier '" + string + "'");
				case ']':
					if (charAt(string, i + 1) == ']') {
						++i;
						// fall through
					} else {
						builder.segmentComplete(null, Olap4jUtil.replace(string.substring(start, i), "]]", "]"),
								Quoting.QUOTED, syntax);
						++i;
						state = AFTER_SEG;
						break;
					}
				default:
					// Carry on reading.
					++i;
				}
				break;

			case AFTER_SEG:
				switch (c) {
				case ' ':
					// Skip over any spaces
					// TODO: test this case: '[foo] . [bar]'
					++i;
					break;
				case '.':
					state = BEFORE_SEG;
					syntax = Builder.Syntax.NAME;
					++i;
					break;
				case '&':
					state = BEFORE_SEG;
					// Roll the syntax - NAME=>FIRST_KEY=>NEXT_KEY=>NEXT_KEY...
					if (syntax == Builder.Syntax.NAME) {
						syntax = Builder.Syntax.FIRST_KEY;
					} else {
						syntax = Builder.Syntax.NEXT_KEY;
					}
					++i;
					break;
				default:
					// We're not looking at the start of a segment. Parse
					// the member we've seen so far, then return.
					break loop;
				}
				break;

			default:
				throw new AssertionError("unexpected state: " + state);
			}
		}

		switch (state) {
		case START:
			return i;
		case BEFORE_SEG:
			throw new IllegalArgumentException("Expected identifier after '.', in member identifier '" + string + "'");
		case IN_BRACKET_SEG:
			throw new IllegalArgumentException("Expected ']', in member identifier '" + string + "'");
		}
		// End of member.
		builder.memberComplete();
		return i;
	}

	private static IllegalArgumentException fail(String string, int i, String expecting) {
		throw new IllegalArgumentException("expected '" + expecting + "' at position " + i + " in '" + string + "'");
	}

	/**
	 * Parses an MDX identifier such as <code>[Foo].[Bar].Baz.&Key&Key2</code> and returns the result as a list of
	 * segments.
	 * 
	 * @param s
	 *            MDX identifier
	 * @return List of segments
	 */
	public static List<IdentifierSegment> parseIdentifier(String s) {
		final MemberBuilder builder = new MemberBuilder();
		int i = parseMember(builder, s, 0);
		if (i < s.length()) {
			throw new IllegalArgumentException("Invalid member identifier '" + s + "'");
		}
		return builder.segmentList;
	}

	/**
	 * Parses a string consisting of a sequence of MDX identifiers and returns the result as a list of compound
	 * identifiers, each of which is a list of segments.
	 * 
	 * <p>
	 * For example, parseIdentifierList("{foo.bar, baz}") returns { {"foo", "bar"}, {"baz"} }.
	 * 
	 * <p>
	 * The braces around the list are optional; parseIdentifierList("foo.bar, baz") returns the same result as the
	 * previous example.
	 * 
	 * @param s
	 *            MDX identifier list
	 * @return List of lists of segments
	 */
	public static List<List<IdentifierSegment>> parseIdentifierList(String s) {
		final MemberListBuilder builder = new MemberListBuilder();
		parseMemberList(builder, s);
		return builder.list;
	}

	/**
	 * Callback that is called on completion of a structural element like a member or tuple.
	 * 
	 * <p>
	 * Implementations might create a list of members or just create a list of unresolved names.
	 */
	public interface Builder {
		/**
		 * Called when a tuple is complete.
		 */
		void tupleComplete();

		/**
		 * Called when a member is complete.
		 */
		void memberComplete();

		/**
		 * Called when a segment is complete.
		 * 
		 * <p>
		 * For example, the identifier {@code [Time].1997.[Jan].&31} contains four name segments: "[Time]", "1997",
		 * "[Jan]" and "31". The first and third are quoted; the last has an ampersand signifying that it is a key.
		 * 
		 * @param region
		 *            Region of source code
		 * @param name
		 *            Name
		 * @param quoting
		 *            Quoting style
		 * @param syntax
		 *            Whether this is a name segment, first part of a key segment, or continuation of a key segment
		 */
		void segmentComplete(ParseRegion region, String name, Quoting quoting, Syntax syntax);

		enum Syntax {
			NAME, FIRST_KEY, NEXT_KEY
		}
	}

	/**
	 * Implementation of {@link org.olap4j.impl.IdentifierParser.Builder} that collects the segments that make up the
	 * name of a member in a list. It cannot handle tuples or lists of members.
	 */
	public static class MemberBuilder implements Builder {
		protected final List<NameSegment> subSegments;
		protected final List<IdentifierSegment> segmentList;

		public MemberBuilder() {
			segmentList = new ArrayList<IdentifierSegment>();
			subSegments = new ArrayList<NameSegment>();
		}

		public void tupleComplete() {
			throw new UnsupportedOperationException();
		}

		public void memberComplete() {
			flushSubSegments();
		}

		private void flushSubSegments() {
			if (!subSegments.isEmpty()) {
				segmentList.add(new KeySegment(subSegments));
				subSegments.clear();
			}
		}

		public void segmentComplete(ParseRegion region, String name, Quoting quoting, Syntax syntax) {
			final NameSegment segment = new NameSegment(region, name, quoting);
			if (syntax != Syntax.NEXT_KEY) {
				// If we were building a previous key, write it out.
				// E.g. [Foo].&1&2.&3&4&5.
				flushSubSegments();
			}
			if (syntax == Syntax.NAME) {
				segmentList.add(segment);
			} else {
				subSegments.add(segment);
			}
		}
	}

	/**
	 * Implementation of {@link org.olap4j.impl.IdentifierParser.Builder} that collects segments into members, then
	 * collects members into lists.
	 */
	public static class MemberListBuilder extends MemberBuilder {
		final List<List<IdentifierSegment>> list = new ArrayList<List<IdentifierSegment>>();

		public void memberComplete() {
			super.memberComplete();
			list.add(new ArrayList<IdentifierSegment>(segmentList));
			segmentList.clear();
		}
	}
}

// End IdentifierParser.java
